package uuid

import (
	"errors"
	"math/rand"
	"sync"
	"time"
)

// +------------------------------------------------------------------------+
// | 14 Bit ProductID | 40 Bit Timestamp |  5 Bit Sequence ID | 5 Bit Rand |
// +-----------------------------------------------------------------------+

type SnowFlake struct {
	lastTimestamp uint64
	sequence      uint32
	productId     uint32
	rand          uint32
	seed          int64
	lock          sync.Mutex
}

const (
	epoch            int64 = 1621760432373
	numProductBits         = 10
	numTimestampBits       = 42
	numSequenceBits        = 4
	numRandBits            = 6
	MaxTimestamp           = -1 ^ (-1 << numTimestampBits)
	MaxSequence            = -1 ^ (-1 << numSequenceBits)
	MaxRand                = -1 ^ (-1 << numRandBits)
)

func (sf *SnowFlake) pack() uint64 {
	// fmt.Println("productId:", sf.productId)
	uuid := uint64(sf.productId) << (numTimestampBits + numSequenceBits + numRandBits)
	// fmt.Println("uuid:", uuid)
	uuid = uuid | (sf.lastTimestamp << (numSequenceBits + numRandBits))
	uuid = uuid | (uint64(sf.sequence) << numRandBits)
	uuid = uuid | uint64(sf.rand)

	return uuid
}

// waitNextMilli if that microsecond is full
// wait for the next microsecond
func (sf *SnowFlake) waitNextMilli(ts uint64) uint64 {
	for ts == sf.lastTimestamp {
		time.Sleep(100 * time.Microsecond)
		ts = timestamp()
	}
	return ts
}

// timestamp
func timestamp() uint64 {
	return uint64(time.Now().UnixNano()/int64(1000000) - epoch)
}

// Next creates and returns a unique snowflake ID
func (sf *SnowFlake) Generate() (uint64, error) {
	sf.lock.Lock()
	defer sf.lock.Unlock()

	ts := timestamp()
	if ts == sf.lastTimestamp {
		sf.sequence = (sf.sequence + 1) & MaxSequence
		if sf.sequence == 0 {
			ts = sf.waitNextMilli(ts)
		}
	} else {
		sf.sequence = 0
	}

	if ts < sf.lastTimestamp || ts > MaxTimestamp {
		return 0, errors.New("invalid system clock")
	}

	if time.Now().Unix() > sf.seed {
		sf.seed = time.Now().Unix()
	} else {
		sf.seed = sf.seed + 100
	}

	rand.New(rand.NewSource(sf.seed))
	sf.rand = rand.Uint32() % MaxRand

	sf.lastTimestamp = ts
	return sf.pack(), nil
}
